Java Annotation

一、什么是注解


Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。

Java 注解用于为 Java 程序提供元数据。作为元数据,注解不直接影响代码的执行,但也有一些注解实际上可以用于这一目的。

什么是元数据,即一种描述数据的数据。所以可以说注解是描述源代码的数据。简单理解注解可以看出一个个标签,用来标记你的代码,是一种应用于类,方法,参数,变量,构造器及包的一种特殊修饰符。

注解和 class、interface 一样也是一种类型,通过 @interface 定义。如下:

1
2
3
4
5
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}

使用:

1
2
3
@TestAnnotation
public class Test {
}

二、元注解


元注解就是注解到注解上的注解,或者说元注解是一种基本注解,它能用来注解其他注解。我们可以将元注解看成一种特殊的修饰符,用来解释说明注解,它是注解的元数据。例如上例中的 @Retention 等。

元注解一共用 5 种:@Retention、@Documented、@Target、@Inherited、@Repeatable。

1、@Retention

Retention 意为保留期,@Retention 用来解释说明一个注解的存活周期。@Retention 取值:

  • RetentionPolicy.SOURCE
    注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
  • RetentionPolicy.CLASS
    注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
  • RetentionPolicy.RUNTIME
    注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

示例:

1
2
3
@Retention(RetentionPolicy.CLASS)
public @interface TestAnnotation {
}

2、@Documented

用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 此类的工具文档化。Documented 是一个标记注解,没有成员。ANDROID 提供了的 NonNull 注解的源码如下:

1
2
3
4
5
@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE})
public @interface NonNull {
}

3、@Target

指定注解应用的地方,用来限定注解的应用场景(类,方法,参数等等)。不使用 @Target 注解则默认不限制。其取值如下:

取值 描述
ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
ElementType.CONSTRUCTOR 可以给构造方法进行注解
ElementType.FIELD 可以给属性进行注解
ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
ElementType.METHOD 可以给方法进行注解
ElementType.PACKAGE 可以给一个包进行注解
ElementType.PARAMETER 可以给一个方法内的参数进行注解
ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举

示例:ANDROID 中的 @LayoutRes 注解,限定为布局资源。

1
2
3
4
5
6
7
8
9
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
public @interface LayoutRes {
}

4、@Inherited

标记这个注解是继承于哪个注解类。只能被用来标注 “Annotation 类型”,它所标注的 Annotation 具有继承性。当一个超类被 @Inherited 注解的注解(A 注解)进行过注解的话,如果它的子类没有被如何其他注解进行注解,那么这个子类就继承了超类的注解(A 注解)。示例如下:

1
2
3
4
5
6
7
8
9
10
11
@Retention(RetentionPolicy.CLASS)
@Inherited
public @interface TestAnnotation {
}
@TestAnnotation
public class TestA {
}
public class TestB extends TestA{
}

TestAnnotation 被 @Retention 注解,类 TestA 被 @TestAnnotation 注解,类 TestB 继承类 TestA,那么类 TestB 也拥有 TestAnnotation注解。

@Inherited annotation 类型是被标注过的 class 的子类所继承。类并不从它所实现的接口继承 annotation,方法并不从它所重载的方法继承 annotation。

5、@Repeatable

@Repeatable 是自然可重复的意思,这是 Java 8 加进来的新特性。
在需要对同一种注解多次使用时,往往需要借助 @Repeatable。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Roles {
Role[] value();
}
@Repeatable(Roles.class)
public @interface Role {
String role() default "";
}
@Role(role="husband")
@Role(role="father")
@Role(role="son")
public class Person {
}

上面的代码 @Repeatable 注解了 Role ,@Repeatable 后面括号中的类相当于一个容器注解。

什么是容器注解?本身也是注解,用来存放其他注解。按照规定,它里面必须要有一个 value 的属性,属性类型是一个被 @Repeatable 注解过的注解数组。

@Role(role=”husband”),role=”husband” 表示给 Role 这个注解的 role 属性赋值,关于注解的属性下面会说明。Person 类需要多次使用 @Role 注解,所以这里使用 @Repeatable 注解 @Role。

测试一下注解效果:

1
2
3
4
5
6
Annotation[] annotations = Person.class.getAnnotations();
System.out.println(annotations.length);
Roles p1 = (Roles) annotations[0];
for (Role t : p1.value()) {
System.out.println(t.role());
}

打印如下:

1
2
3
4
1
husband
father
son

三、注解的属性


注解的属性也叫成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无参的方法”的形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。

注解的属性类型必须以下几种:8 种基本类型(byte、boolean、char、short、int、long、float、double)和 String、Enum、Class、annotation 类型,以及这些类型的数组。

1
2
3
4
public @interface TestAnnotation {
int id();
String msg();
}

上面代码定义 TestAnnotation 这个注解有 id 和 msg 两个属性。在使用的时候我们需要给它们赋值。

赋值方式:括号内以 value=“” 的形式赋值,多个属性以逗号隔开。

1
2
3
@TestAnnotation(id = 1, msg = "注解测试")
public class Test {
}

注解中可以设置默认值,默认值用 default 关键字指定。使用注解时对于指定了默认值的属性,如果不需要修改,可以不赋值。

1
2
3
4
5
6
7
8
9
public @interface TestAnnotation {
int id() default 0;
String msg() default "msg";
}
// 使用默认值
@TestAnnotation()
public class Test {
}

当一个注解只有一个属性且属性名为 value 时,使用此注解可以省略括号内的属性名直接赋值:

1
2
3
4
5
6
7
public @interface TestAnnotation {
String value();
}
@TestAnnotation("1")
public class Test {
}

如果注解没有属性,括号也可以省略:

1
2
3
4
5
6
public @interface TestAnnotation {
}
@TestAnnotation
public class Test {
}

四、JDK 内置注解


Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中,外加 JDK 7 以后新增 3个,共 10 个。分别是:@Override、@Deprecated、@SuppressWarnings;@Retention、@Documented、@Target、@Inherited;@SafeVarargs、@FunctionalInterface、@Repeatable。

取值 描述
作用在代码的注解:
@Override 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
@Deprecated 标记过时元素的注解,用来标识类,方法或者变量已过时,不建议使用。调用过时方法时编译器会提醒。
@SuppressWarnings 指示编译器去忽略注解中声明的警告。
作用在其他注解的注解:
@Retention 标识这个注解怎么保存,是只在代码中,还是编入 class 文件中,或者是在运行时可以通过反射访问。
@Documented 标记这些注解是否包含在用户文档中。
@Target 标记这个注解应该是哪种 Java 成员。
@Inherited 标记这个注解是继承于哪个注解类(默认注解没有继承于任何子类)
从 JAVA 7 开始新增:
@SafeVarargs JAVA 7,参数安全类型注解。它的目的是提醒开发者不要用参数做一些不安全的操作,它的存在会阻止编译器产生 unchecked 这样的警告。
@FunctionalInterface JAVA 8,标识一个匿名函数或函数式接口。
@Repeatable JAVA 8,标识某注解可以在同一个声明上使用多次。

五、注解的提取与应用


注解通过反射获取,通过 Class 对象的方法获取注解。常用的三个方法:
isAnnotationPresent
方法判断是否应用了某个注解。

1
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {...}

getAnnotation
方法获取指定类型的注解。

1
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {...}

getAnnotations
方法获取注解到当前元素上的所有注解。

1
public Annotation[] getAnnotations() {...}

以运行时注解为例:获取下面 TestAnnotation 注解的 name 属性内容,可以如下获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
@interface TestAnnotation {
String name();
}
@TestAnnotation(name = "先小涛")
public class Person {
}
// 测试
private void test() {
boolean hasAnnotation = Person.class.isAnnotationPresent(TestAnnotation.class);
if (hasAnnotation){
TestAnnotation annotation = Person.class.getAnnotation(TestAnnotation.class);
Log.d(TAG, annotation.name());
}
}

打印结果:

1
2020-03-09 02:42:35.379 26182-26182/com.xxt.xtest D/xian: 先小涛

方法和属性也可以借助返回来获取注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Retention(RetentionPolicy.RUNTIME)
@interface TestAnnotation {
String msg();
}
@TestAnnotation(msg="注解类")
public class Person {
@TestAnnotation(msg="注解成员变量")
private String name;
@TestAnnotation(msg="注解方法")
private void setName(String name) {
this.name = name;
}
}
// 测试
private void test() {
boolean hasAnnotation = Person.class.isAnnotationPresent(TestAnnotation.class);
if (hasAnnotation) {
TestAnnotation annotation = Person.class.getAnnotation(TestAnnotation.class);
if (annotation != null) {
Log.d(TAG, annotation.msg());
}
}
try {
// 属性获取注解
Field nameField = Person.class.getDeclaredField("name");
nameField.setAccessible(true);
TestAnnotation annotation1 = nameField.getAnnotation(TestAnnotation.class);
if (annotation1 != null) {
Log.d(TAG, annotation1.msg());
}
// 方法获取注解
Method setNameMethod = Person.class.getDeclaredMethod("setName", String.class);
TestAnnotation annotation2 = setNameMethod.getAnnotation(TestAnnotation.class);
if (annotation2 != null) {
Log.d(TAG, annotation2.msg());
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}

打印结果:

1
2
3
2020-03-09 02:53:23.584 27085-27085/com.xxt.xtest D/xian: 注解类
2020-03-09 02:53:23.584 27085-27085/com.xxt.xtest D/xian: 注解成员变量
2020-03-09 02:53:23.584 27085-27085/com.xxt.xtest D/xian: 注解方法

注意:如果一个注解要想在运行时被提取,那么@Retention(RetentionPolicy.RUNTIME) 是必须的。

注解的应用:

  • 提供信息给编译器:编译器可以通过注解来探测错误和警告信息。
  • 编译阶段处理:软件工具可以利用注解信息来自动生成代码,HTML文档或者做其他相应处理。
  • 运行时的处理: 某些注解可以在程序运行时接收代码的提取。

当开发者使用注解修饰了类,方法,变量等成员后,注解不会自己生效,必须由开发者提供对应的代码来提取处理注解信息。

这些用来提取和处理注解信息的代码统称为APT(Annotation Processing Tool),注解处理器,它用来在编译时扫描和处理注解。

总结来说,注解(Annotation)相对于一种标记,注解的应用就是编译器,开发工具或者程序通过反射来提取你的类和各种元素有无这种标记,有某种标记就去做相应的处理。

参考文章:
Java 注解 annotation
Java 注解(Annotation)